package com.carey.blog.jsonrpc.impl;

import static com.carey.blog.model.Article.ARTICLE;
import static com.carey.blog.model.Article.ARTICLES;
import static com.carey.blog.model.Article.ARTICLE_ABSTRACT;
import static com.carey.blog.model.Article.ARTICLE_AUTHOR_EMAIL;
import static com.carey.blog.model.Article.ARTICLE_COMMENT_COUNT;
import static com.carey.blog.model.Article.ARTICLE_CONTENT;
import static com.carey.blog.model.Article.ARTICLE_CREATE_DATE;
import static com.carey.blog.model.Article.ARTICLE_HAD_BEEN_PUBLISHED;
import static com.carey.blog.model.Article.ARTICLE_IS_PUBLISHED;
import static com.carey.blog.model.Article.ARTICLE_PERMALINK;
import static com.carey.blog.model.Article.ARTICLE_PUT_TOP;
import static com.carey.blog.model.Article.ARTICLE_RANDOM_DOUBLE;
import static com.carey.blog.model.Article.ARTICLE_SIGN_REF;
import static com.carey.blog.model.Article.ARTICLE_TAGS_REF;
import static com.carey.blog.model.Article.ARTICLE_UPDATE_DATE;
import static com.carey.blog.model.Article.ARTICLE_VIEW_COUNT;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.carey.blog.action.StatusCodes;
import com.carey.blog.event.EventTypes;
import com.carey.blog.framework.latke.Keys;
import com.carey.blog.framework.latke.action.ActionException;
import com.carey.blog.framework.latke.action.util.PageCaches;
import com.carey.blog.framework.latke.action.util.Paginator;
import com.carey.blog.framework.latke.event.Event;
import com.carey.blog.framework.latke.event.EventException;
import com.carey.blog.framework.latke.event.EventManager;
import com.carey.blog.framework.latke.model.Pagination;
import com.carey.blog.framework.latke.model.User;
import com.carey.blog.framework.latke.repository.FilterOperator;
import com.carey.blog.framework.latke.repository.Query;
import com.carey.blog.framework.latke.repository.SortDirection;
import com.carey.blog.framework.latke.repository.Transaction;
import com.carey.blog.framework.latke.util.Strings;
import com.carey.blog.jsonrpc.AbstractGAEJSONRpcService;
import com.carey.blog.model.Common;
import com.carey.blog.model.Preference;
import com.carey.blog.model.Sign;
import com.carey.blog.model.Tag;
import com.carey.blog.repository.ArticleRepository;
import com.carey.blog.repository.ArticleSignRepository;
import com.carey.blog.repository.TagArticleRepository;
import com.carey.blog.repository.TagRepository;
import com.carey.blog.repository.impl.ArticleGAERepository;
import com.carey.blog.repository.impl.ArticleSignGAERepository;
import com.carey.blog.repository.impl.TagArticleGAERepository;
import com.carey.blog.repository.impl.TagGAERepository;
import com.carey.blog.util.ArchiveDates;
import com.carey.blog.util.Articles;
import com.carey.blog.util.Permalinks;
import com.carey.blog.util.Preferences;
import com.carey.blog.util.Statistics;
import com.carey.blog.util.Tags;
import com.carey.blog.util.TimeZones;
import com.carey.blog.util.Users;

/**
 * Article service for JavaScript client.
 * 
 */
public final class ArticleService extends AbstractGAEJSONRpcService {

	/**
	 * Logger.
	 */
	private static final Logger LOGGER = Logger.getLogger(ArticleService.class
			.getName());
	/**
	 * Article repository.
	 */
	private ArticleRepository articleRepository = ArticleGAERepository
			.getInstance();
	/**
	 * Tag repository.
	 */
	private TagRepository tagRepository = TagGAERepository.getInstance();
	/**
	 * Tag-Article repository.
	 */
	private TagArticleRepository tagArticleRepository = TagArticleGAERepository
			.getInstance();
	/**
	 * Article-Sign repository.
	 */
	private ArticleSignRepository articleSignRepository = ArticleSignGAERepository
			.getInstance();
	/**
	 * Event manager.
	 */
	private EventManager eventManager = EventManager.getInstance();
	/**
	 * Tag utilities.
	 */
	private Tags tagUtils = Tags.getInstance();
	/**
	 * Article utilities.
	 */
	private Articles articleUtils = Articles.getInstance();
	/**
	 * Statistic utilities.
	 */
	private Statistics statistics = Statistics.getInstance();
	/**
	 * Archive date utilities.
	 */
	private ArchiveDates archiveDateUtils = ArchiveDates.getInstance();
	/**
	 * Preference utilities.
	 */
	private Preferences preferenceUtils = Preferences.getInstance();
	/**
	 * Permalink utilities.
	 */
	private Permalinks permalinks = Permalinks.getInstance();
	/**
	 * User utilities.
	 */
	private Users userUtils = Users.getInstance();
	/**
	 * Time zone utilities.
	 */
	private TimeZones timeZoneUtils = TimeZones.getInstance();
	/**
	 * Permalink date format(yyyy/MM/dd).
	 */
	public static final DateFormat PERMALINK_FORMAT = new SimpleDateFormat(
			"yyyy/MM/dd");

	/**
	 * Adds an article from the specified request json object and http servlet
	 * request.
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "article": {
	 *         "articleTitle": "",
	 *         "articleAbstract": "",
	 *         "articleContent": "",
	 *         "articleTags": "tag1,tag2,tag3",
	 *         "articlePermalink": "", // optional
	 *         "articleIsPublished": boolean,
	 *         "articleSign_oId": "" // optional
	 *     }
	 * }
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *     "oId": generatedArticleId
	 *     "status": {
	 *         "code": "ADD_ARTICLE_SUCC",
	 *         "events": { // optional
	 *             "blogSyncCSDNBlog": {
	 *                 "code": "",
	 *                 "msg": "" // optional
	 *             },
	 *             ....
	 *         }
	 *     }
	 * }
	 * </pre>
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 */
	public JSONObject addArticle(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();

		if (!userUtils.isLoggedIn()) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return ret;
		}

		final Transaction transaction = articleRepository.beginTransaction();
		final JSONObject status = new JSONObject();
		try {
			ret.put(Keys.STATUS, status);
			if (userUtils.isCollaborateAdmin()) {
				status.put(Keys.CODE, StatusCodes.UPDATE_ARTICLE_FAIL_FORBIDDEN);

				return ret;
			}

			final JSONObject article = requestJSONObject.getJSONObject(ARTICLE);
			
			// Step 1: Add tags
			final String tagsString = article.getString(ARTICLE_TAGS_REF);
			final String[] tagTitles = tagsString.split(",");
			final JSONArray tags = tagUtils.tag(tagTitles, article);
			
			// Step 2; Set comment/view count to 0
			article.put(ARTICLE_COMMENT_COUNT, 0);
			article.put(ARTICLE_VIEW_COUNT, 0);
			
			// Step 3: Set create/update date
			final JSONObject preference = preferenceUtils.getPreference();
			final String timeZoneId = preference
					.getString(Preference.TIME_ZONE_ID);
			final Date date = timeZoneUtils.getTime(timeZoneId);
			article.put(ARTICLE_UPDATE_DATE, date);
			article.put(ARTICLE_CREATE_DATE, date);
			
			// Step 4: Set put top to false
			article.put(ARTICLE_PUT_TOP, false);
			
			// Step 5: Add article
			final String articleId = articleRepository.add(article);
			ret.put(Keys.OBJECT_ID, articleId);
			
			// Step 6: Add tag-article relations
			articleUtils.addTagArticleRelation(tags, article);
			
			// Step 7: Inc blog article count statictis
			statistics.incBlogArticleCount();
			if (article.getBoolean(ARTICLE_IS_PUBLISHED)) {
				statistics.incPublishedBlogArticleCount();
			}
			
			// Step 8: Add archive date-article relations
			archiveDateUtils.archiveDate(article);
			
			// Step 9: Set permalink
			String permalink = article.optString(ARTICLE_PERMALINK);
			if (Strings.isEmptyOrNull(permalink)) {
				permalink = "/articles/" + PERMALINK_FORMAT.format(date) + "/"
						+ articleId + ".html";
			}

			if (!permalink.startsWith("/")) {
				permalink = "/" + permalink;
			}

			if (permalinks.exist(permalink)) {
				status.put(Keys.CODE,
						StatusCodes.ADD_ARTICLE_FAIL_DUPLICATED_PERMALINK);

				throw new Exception(
						"Add article fail, caused by duplicated permalink["
								+ permalink + "]");
			}
			article.put(ARTICLE_PERMALINK, permalink);
			
			// Step 10: Add article-sign relation
			final String signId = article.getString(ARTICLE_SIGN_REF + "_"
					+ Keys.OBJECT_ID);
			articleUtils.addArticleSignRelation(signId, articleId);
			article.remove(ARTICLE_SIGN_REF + "_" + Keys.OBJECT_ID);
			
			// Step 11: Set had been published status
			article.put(ARTICLE_HAD_BEEN_PUBLISHED, false);
			if (article.getBoolean(ARTICLE_IS_PUBLISHED)) {
				// Publish it directly
				article.put(ARTICLE_HAD_BEEN_PUBLISHED, true);
			}
			
			// Step 12: Set author email
			final String authorEmail = userUtils.getCurrentUser().getString(
					User.USER_EMAIL);
			article.put(ARTICLE_AUTHOR_EMAIL, authorEmail);
			
			// Step 13: Set random double
			article.put(ARTICLE_RANDOM_DOUBLE, Math.random());
			
			// Step 14: Update article
			articleRepository.update(articleId, article);

			if (article.getBoolean(ARTICLE_IS_PUBLISHED)) {
				// Fire add article event
				final JSONObject eventData = new JSONObject();
				eventData.put(ARTICLE, article);
				eventData.put(Keys.RESULTS, ret);
				eventManager.fireEventSynchronously(new Event<JSONObject>(
						EventTypes.ADD_ARTICLE, eventData));
			}

			transaction.commit();

			status.put(Keys.CODE, StatusCodes.ADD_ARTICLE_SUCC);
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			if (transaction.isActive()) {
				transaction.rollback();
			}

			return ret;
		}

		PageCaches.removeAll();

		return ret;
	}

	/**
	 * Gets an article by the specified request json object.
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "oId": ""
	 * }
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *     "oId": "",
	 *     "articleTitle": "",
	 *     "articleAbstract": "",
	 *     "articleContent": "",
	 *     "articlePermalink": "",
	 *     "articleHadBeenPublished": boolean,
	 *     "articleTags": [{
	 *         "oId": "",
	 *         "tagTitle": ""
	 *     }, ....],
	 *     "articleSign_oId": "",
	 *     "signs": [{
	 *         "oId": "",
	 *         "signHTML": ""
	 *     }, ....]
	 *     "sc": "GET_ARTICLE_SUCC"
	 * }
	 * </pre>
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 */
	public JSONObject getArticle(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();
		if (!userUtils.isLoggedIn()) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return ret;
		}

		try {
			final String articleId = requestJSONObject
					.getString(Keys.OBJECT_ID);
			final JSONObject article = articleRepository.get(articleId);
			ret.put(ARTICLE, article);

			final JSONArray tags = new JSONArray();
			final List<JSONObject> tagArticleRelations = tagArticleRepository
					.getByArticleId(articleId);
			for (int i = 0; i < tagArticleRelations.size(); i++) {
				final JSONObject tagArticleRelation = tagArticleRelations
						.get(i);
				final String tagId = tagArticleRelation.getString(Tag.TAG + "_"
						+ Keys.OBJECT_ID);
				final JSONObject tag = tagRepository.get(tagId);

				tags.put(tag);
			}
			article.put(ARTICLE_TAGS_REF, tags);

			final JSONObject preference = preferenceUtils.getPreference();
			final String signId = articleUtils.getSign(articleId, preference)
					.getString(Keys.OBJECT_ID);
			article.put(ARTICLE_SIGN_REF + "_" + Keys.OBJECT_ID, signId);

			final JSONArray signs = new JSONArray(
					preference.getString(Preference.SIGNS));
			article.put(Sign.SIGNS, signs);

			// Remove unused properties
			article.remove(ARTICLE_AUTHOR_EMAIL);
			article.remove(ARTICLE_COMMENT_COUNT);
			article.remove(ARTICLE_CREATE_DATE);
			article.remove(ARTICLE_IS_PUBLISHED);
			article.remove(ARTICLE_PUT_TOP);
			article.remove(ARTICLE_UPDATE_DATE);
			article.remove(ARTICLE_VIEW_COUNT);
			article.remove(ARTICLE_RANDOM_DOUBLE);

			ret.put(Keys.STATUS_CODE, StatusCodes.GET_ARTICLE_SUCC);

			LOGGER.log(Level.FINER, "Got an article[oId={0}]", articleId);
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw new ActionException(e);
		}

		return ret;
	}

	/**
	 * Gets articles(by crate date descending) by the specified request json
	 * object.
	 * 
	 * <p>
	 * If the property "articleIsPublished" of the specified request json object
	 * is {@code true}, the returned articles all are unpublished, {@code false}
	 * otherwise.
	 * </p>
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "paginationCurrentPageNum": 1,
	 *     "paginationPageSize": 20,
	 *     "paginationWindowSize": 10,
	 *     "articleIsPublished": boolean
	 * }, see {@link Pagination} for more details
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *     "pagination": {
	 *         "paginationPageCount": 100,
	 *         "paginationPageNums": [1, 2, 3, 4, 5]
	 *     },
	 *     "articles": [{
	 *         "oId": "",
	 *         "articleTitle": "",
	 *         "articleCommentCount": int,
	 *         "articleCreateDate"; java.util.Date,
	 *         "articleViewCount": int,
	 *         "articleTags": "tag1, tag2, ....",
	 *         "articlePutTop": boolean,
	 *         "articleIsPublished": boolean
	 *      }, ....]
	 *     "sc": "GET_ARTICLES_SUCC"
	 * }
	 * </pre>
	 * 
	 *         , order by article update date and sticky(put top).
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 * @see Pagination
	 */
	public JSONObject getArticles(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();
		if (!userUtils.isLoggedIn()) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return ret;
		}

		try {
			final int currentPageNum = requestJSONObject
					.getInt(Pagination.PAGINATION_CURRENT_PAGE_NUM);
			final int pageSize = requestJSONObject
					.getInt(Pagination.PAGINATION_PAGE_SIZE);
			final int windowSize = requestJSONObject
					.getInt(Pagination.PAGINATION_WINDOW_SIZE);
			final boolean articleIsPublished = requestJSONObject.optBoolean(
					ARTICLE_IS_PUBLISHED, true);

			final Query query = new Query()
					.setCurrentPageNum(currentPageNum)
					.setPageSize(pageSize)
					.addSort(ARTICLE_CREATE_DATE, SortDirection.DESCENDING)
					.addSort(ARTICLE_PUT_TOP, SortDirection.DESCENDING)
					.addFilter(ARTICLE_IS_PUBLISHED, FilterOperator.EQUAL,
							articleIsPublished);
			final JSONObject result = articleRepository.get(query);

			final int pageCount = result.getJSONObject(Pagination.PAGINATION)
					.getInt(Pagination.PAGINATION_PAGE_COUNT);

			final JSONObject pagination = new JSONObject();
			ret.put(Pagination.PAGINATION, pagination);
			final List<Integer> pageNums = Paginator.paginate(currentPageNum,
					pageSize, pageCount, windowSize);
			pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
			pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);

			final JSONArray articles = result.getJSONArray(Keys.RESULTS);

			for (int i = 0; i < articles.length(); i++) {
				final JSONObject article = articles.getJSONObject(i);
				final JSONObject author = articleUtils.getAuthor(article);
				final String authorName = author.getString(User.USER_NAME);
				article.put(Common.AUTHOR_NAME, authorName);

				// Remove unused properties
				article.remove(ARTICLE_CONTENT);
				article.remove(ARTICLE_ABSTRACT);
				article.remove(ARTICLE_UPDATE_DATE);
				article.remove(ARTICLE_AUTHOR_EMAIL);
				article.remove(ARTICLE_HAD_BEEN_PUBLISHED);
				article.remove(ARTICLE_IS_PUBLISHED);
				article.remove(ARTICLE_RANDOM_DOUBLE);
			}
			ret.put(ARTICLES, articles);

			ret.put(Keys.STATUS_CODE, StatusCodes.GET_ARTICLES_SUCC);
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw new ActionException(e);
		}

		return ret;
	}

	/**
	 * Removes an article by the specified request json object.
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "oId": "",
	 * }
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *     "status": {
	 *         "code": "REMOVE_ARTICLE_SUCC",
	 *         "events": { // optional
	 *             "blogSyncCSDNBlog": {
	 *                 "code": "",
	 *                 "msg": "" // optional
	 *             },
	 *             ....
	 *         }
	 *     }
	 * }
	 * </pre>
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 */
	public JSONObject removeArticle(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();
		if (!userUtils.isLoggedIn()) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return ret;
		}

		final Transaction transaction = articleRepository.beginTransaction();
		try {
			final JSONObject status = new JSONObject();
			ret.put(Keys.STATUS, status);

			final String articleId = requestJSONObject
					.getString(Keys.OBJECT_ID);
			if (!userUtils.canAccessArticle(articleId)) {
				status.put(Keys.CODE, StatusCodes.REMOVE_ARTICLE_FAIL_FORBIDDEN);

				return ret;
			}

			LOGGER.log(Level.FINER, "Removing an article[oId={0}]", articleId);
			tagUtils.decTagRefCount(articleId);
			archiveDateUtils.unArchiveDate(articleId);
			articleUtils.removeTagArticleRelations(articleId);
			articleUtils.removeArticleComments(articleId);
			final JSONObject article = articleRepository.get(articleId);
			articleRepository.remove(articleId);
			statistics.decBlogArticleCount();
			if (article.getBoolean(ARTICLE_IS_PUBLISHED)) {
				statistics.decPublishedBlogArticleCount();
			}
			final JSONObject eventData = new JSONObject();
			eventData.put(Keys.OBJECT_ID, articleId);
			eventData.put(Keys.RESULTS, ret);
			try {
				eventManager.fireEventSynchronously(new Event<JSONObject>(
						EventTypes.REMOVE_ARTICLE, eventData));
			} catch (final EventException e) {
				LOGGER.log(Level.SEVERE, e.getMessage(), e);
			}

			transaction.commit();

			status.put(Keys.CODE, StatusCodes.REMOVE_ARTICLE_SUCC);
			LOGGER.log(Level.FINER, "Removed an article[oId={0}]", articleId);
		} catch (final Exception e) {
			if (transaction.isActive()) {
				transaction.rollback();
			}
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw new ActionException(e);
		}

		PageCaches.removeAll();

		return ret;
	}

	/**
	 * Puts an article to top by the specified request json object.
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "oId": "",
	 * }
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *     "sc": "PUT_TOP_ARTICLE_SUCC"
	 * }
	 * </pre>
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 */
	public JSONObject putTopArticle(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();
		if (!userUtils.isAdminLoggedIn()) {
			try {
				ret.put(Keys.STATUS_CODE,
						StatusCodes.PUT_TOP_ARTICLE_FAIL_FORBIDDEN);
			} catch (final JSONException e) {
				LOGGER.log(Level.SEVERE, e.getMessage(), e);
			}

			return ret;
		}
		final Transaction transaction = articleRepository.beginTransaction();
		String articleId = null;
		try {
			articleId = requestJSONObject.getString(Keys.OBJECT_ID);
			final JSONObject topArticle = articleRepository.get(articleId);
			topArticle.put(ARTICLE_PUT_TOP, true);
			articleRepository.update(articleId, topArticle);
			transaction.commit();

			ret.put(Keys.STATUS_CODE, StatusCodes.PUT_TOP_ARTICLE_SUCC);
		} catch (final Exception e) {
			if (transaction.isActive()) {
				transaction.rollback();
			}
			LOGGER.log(Level.SEVERE, "Can't put the article[oId{0}] to top",
					articleId);
			try {
				ret.put(Keys.STATUS_CODE, StatusCodes.PUT_TOP_ARTICLE_FAIL_);
			} catch (final JSONException ex) {
				LOGGER.severe(ex.getMessage());
				throw new ActionException(e);
			}
		}

		PageCaches.removeAll();

		return ret;
	}

	/**
	 * Cancels an article from top by the specified request json object.
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "oId": "",
	 * }
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *     "sc": "CANCEL_TOP_ARTICLE_SUCC"
	 * }
	 * </pre>
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 */
	public JSONObject cancelTopArticle(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();
		if (!userUtils.isAdminLoggedIn()) {
			try {
				ret.put(Keys.STATUS_CODE,
						StatusCodes.CANCEL_TOP_ARTICLE_FAIL_FORBIDDEN);
			} catch (final JSONException e) {
				LOGGER.log(Level.SEVERE, e.getMessage(), e);
			}
			return ret;
		}
		final Transaction transaction = articleRepository.beginTransaction();
		String articleId = null;
		try {
			articleId = requestJSONObject.getString(Keys.OBJECT_ID);
			final JSONObject topArticle = articleRepository.get(articleId);
			topArticle.put(ARTICLE_PUT_TOP, false);
			articleRepository.update(articleId, topArticle);
			transaction.commit();

			ret.put(Keys.STATUS_CODE, StatusCodes.CANCEL_TOP_ARTICLE_SUCC);
		} catch (final Exception e) {
			if (transaction.isActive()) {
				transaction.rollback();
			}
			LOGGER.log(Level.SEVERE,
					"Can't cancel the article[oId{0}] from top", articleId);
			try {
				ret.put(Keys.STATUS_CODE, StatusCodes.CANCEL_TOP_ARTICLE_FAIL_);
			} catch (final JSONException ex) {
				LOGGER.severe(ex.getMessage());
				throw new ActionException(e);
			}
		}

		PageCaches.removeAll();

		return ret;
	}

	/**
	 * Updates an article by the specified request json object.
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "article": {
	 *         "oId": "",
	 *         "articleTitle": "",
	 *         "articleAbstract": "",
	 *         "articleContent": "",
	 *         "articleTags": "tag1,tag2,tag3",
	 *         "articlePermalink": "", // optional
	 *         "articleIsPublished": boolean,
	 *         "articleSign_oId": "" // optional
	 *     }
	 * }
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *     "status": {
	 *         "code": "UPDATE_ARTICLE_SUCC",
	 *         "events": { // optional
	 *             "blogSyncCSDNBlog": {
	 *                 "code": "",
	 *                 "msg": "" // optional
	 *             },
	 *             ....
	 *         }
	 *     }
	 * }
	 * </pre>
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 */
	public JSONObject updateArticle(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();

		if (!userUtils.isLoggedIn()) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return ret;
		}

		final Transaction transaction = articleRepository.beginTransaction();

		final JSONObject status = new JSONObject();
		try {
			ret.put(Keys.STATUS, status);

			if (userUtils.isCollaborateAdmin()) {
				status.put(Keys.CODE, StatusCodes.UPDATE_ARTICLE_FAIL_FORBIDDEN);

				return ret;
			}

			final JSONObject article = requestJSONObject.getJSONObject(ARTICLE);
			final String articleId = article.getString(Keys.OBJECT_ID);
			if (!userUtils.canAccessArticle(articleId)) {
				status.put(Keys.CODE, StatusCodes.UPDATE_ARTICLE_FAIL_FORBIDDEN);

				return ret;
			}
			// Set permalink
			final JSONObject oldArticle = articleRepository.get(articleId);
			final String permalink = getPermalinkForUpdateArticle(oldArticle,
					article, (Date) oldArticle.get(ARTICLE_CREATE_DATE), status);
			article.put(ARTICLE_PERMALINK, permalink);
			// Process tag
			tagUtils.processTagsForArticleUpdate(oldArticle, article);
			// Fill auto properties
			fillAutoProperties(oldArticle, article);
			// Set date
			article.put(ARTICLE_UPDATE_DATE,
					oldArticle.get(ARTICLE_UPDATE_DATE));
			final JSONObject preference = preferenceUtils.getPreference();
			final String timeZoneId = preference
					.getString(Preference.TIME_ZONE_ID);
			final Date date = timeZoneUtils.getTime(timeZoneId);
			if (article.getBoolean(ARTICLE_IS_PUBLISHED)) { // Publish it
				if (articleUtils.hadBeenPublished(oldArticle)) {
					// Edit update date only for published article
					article.put(ARTICLE_UPDATE_DATE, date);
				} else { // This article is a draft and this is the first time
							// to publish it
					article.put(ARTICLE_CREATE_DATE, date);
					article.put(ARTICLE_UPDATE_DATE, date);
					article.put(ARTICLE_HAD_BEEN_PUBLISHED, true);
				}
			} else { // Save as draft
				if (articleUtils.hadBeenPublished(oldArticle)) {
					// Save update date only for published article
					article.put(ARTICLE_UPDATE_DATE, date);
				} else {
					// Reset create/update date to indicate this is an new draft
					article.put(ARTICLE_CREATE_DATE, date);
					article.put(ARTICLE_UPDATE_DATE, date);
				}
			}

			final boolean publishNewArticle = !oldArticle
					.getBoolean(ARTICLE_IS_PUBLISHED)
					&& article.getBoolean(ARTICLE_IS_PUBLISHED);
			// Set statistic
			if (publishNewArticle) {
				// This article is updated from unpublished to published
				statistics.incPublishedBlogArticleCount();
				final int blogCmtCnt = statistics
						.getPublishedBlogCommentCount();
				final int articleCmtCnt = article.getInt(ARTICLE_COMMENT_COUNT);
				statistics.setPublishedBlogCommentCount(blogCmtCnt
						+ articleCmtCnt);
			}
			// Add article-sign relation
			final String signId = article.getString(ARTICLE_SIGN_REF + "_"
					+ Keys.OBJECT_ID);
			final JSONObject articleSignRelation = articleSignRepository
					.getByArticleId(articleId);
			if (null != articleSignRelation) {
				articleSignRepository.remove(articleSignRelation
						.getString(Keys.OBJECT_ID));
			}
			articleUtils.addArticleSignRelation(signId, articleId);
			article.remove(ARTICLE_SIGN_REF + "_" + Keys.OBJECT_ID);
			if (publishNewArticle) {
				archiveDateUtils.incArchiveDatePublishedRefCount(articleId);
			}

			// Update
			articleRepository.update(articleId, article);

			if (publishNewArticle) {
				// Fire add article event
				final JSONObject eventData = new JSONObject();
				eventData.put(ARTICLE, article);
				eventData.put(Keys.RESULTS, ret);
				try {
					eventManager.fireEventSynchronously(new Event<JSONObject>(
							EventTypes.ADD_ARTICLE, eventData));
				} catch (final EventException e) {
					LOGGER.log(Level.SEVERE, e.getMessage(), e);
				}
			} else {
				// Fire update article event
				final JSONObject eventData = new JSONObject();
				eventData.put(ARTICLE, article);
				eventData.put(Keys.RESULTS, ret);
				try {
					eventManager.fireEventSynchronously(new Event<JSONObject>(
							EventTypes.UPDATE_ARTICLE, eventData));
				} catch (final EventException e) {
					LOGGER.log(Level.SEVERE, e.getMessage(), e);
				}
			}

			transaction.commit();

			status.put(Keys.CODE, StatusCodes.UPDATE_ARTICLE_SUCC);
			ret.put(Keys.STATUS, status);
			LOGGER.log(Level.FINER, "Updated an article[oId={0}]", articleId);
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);

			if (transaction.isActive()) {
				transaction.rollback();
			}

			return ret;
		}

		PageCaches.removeAll();

		return ret;
	}

	/**
	 * Fills 'auto' properties for the specified article and old article.
	 * 
	 * <p>
	 * Some properties of an article are not been changed while article
	 * updating, these properties are called 'auto' properties.
	 * </p>
	 * 
	 * <p>
	 * The property(named
	 * {@value com.carey.blog.model.Article#ARTICLE_RANDOM_DOUBLE}) of the
	 * specified article will be regenerated.
	 * </p>
	 * 
	 * @param oldArticle
	 *            the specified old article
	 * @param article
	 *            the specified article
	 * @throws JSONException
	 *             json exception
	 */
	private void fillAutoProperties(final JSONObject oldArticle,
			final JSONObject article) throws JSONException {

		final Date createDate = (Date) oldArticle.get(ARTICLE_CREATE_DATE);
		article.put(ARTICLE_CREATE_DATE, createDate);
		article.put(ARTICLE_COMMENT_COUNT,
				oldArticle.getInt(ARTICLE_COMMENT_COUNT));
		article.put(ARTICLE_VIEW_COUNT, oldArticle.getInt(ARTICLE_VIEW_COUNT));
		article.put(ARTICLE_PUT_TOP, oldArticle.getBoolean(ARTICLE_PUT_TOP));
		article.put(ARTICLE_HAD_BEEN_PUBLISHED,
				oldArticle.getBoolean(ARTICLE_HAD_BEEN_PUBLISHED));
		article.put(ARTICLE_AUTHOR_EMAIL,
				oldArticle.getString(ARTICLE_AUTHOR_EMAIL));
		article.put(ARTICLE_RANDOM_DOUBLE, Math.random());
	}

	/**
	 * Cancels publish an article by the specified request json object.
	 * 
	 * @param requestJSONObject
	 *            the specified request json object, for example,
	 * 
	 *            <pre>
	 * {
	 *     "oId": "",
	 * }
	 * </pre>
	 * @param request
	 *            the specified http servlet request
	 * @param response
	 *            the specified http servlet response
	 * @return for example,
	 * 
	 *         <pre>
	 * {
	 *   "sc": "CANCEL_PUBLISH_ARTICLE_SUCC"
	 * }
	 * </pre>
	 * @throws ActionException
	 *             action exception
	 * @throws IOException
	 *             io exception
	 */
	public JSONObject cancelPublishArticle(final JSONObject requestJSONObject,
			final HttpServletRequest request, final HttpServletResponse response)
			throws ActionException, IOException {
		final JSONObject ret = new JSONObject();
		if (!userUtils.isLoggedIn()) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return ret;
		}

		final Transaction transaction = articleRepository.beginTransaction();
		try {
			final String articleId = requestJSONObject
					.getString(Keys.OBJECT_ID);

			if (!userUtils.canAccessArticle(articleId)) {
				ret.put(Keys.STATUS_CODE,
						StatusCodes.CANCEL_PUBLISH_ARTICLE_FAIL_FORBIDDEN);

				return ret;
			}

			final JSONObject article = articleRepository.get(articleId);
			article.put(ARTICLE_IS_PUBLISHED, false);
			tagUtils.decTagPublishedRefCount(articleId);
			archiveDateUtils.decArchiveDatePublishedRefCount(articleId);
			articleRepository.update(articleId, article);
			statistics.decPublishedBlogArticleCount();
			final int blogCmtCnt = statistics.getPublishedBlogCommentCount();
			final int articleCmtCnt = article.getInt(ARTICLE_COMMENT_COUNT);
			statistics.setPublishedBlogCommentCount(blogCmtCnt - articleCmtCnt);

			transaction.commit();

			ret.put(Keys.STATUS_CODE, StatusCodes.CANCEL_PUBLISH_ARTICLE_SUCC);
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			if (transaction.isActive()) {
				transaction.rollback();
			}

			try {
				ret.put(Keys.STATUS_CODE,
						StatusCodes.CANCEL_PUBLISH_ARTICLE_FAIL_);
			} catch (final JSONException ex) {
				LOGGER.severe(ex.getMessage());
				throw new ActionException(e);
			}

			return ret;
		}

		PageCaches.removeAll();

		return ret;
	}

	/**
	 * Gets article permalink for updating article with the specified old
	 * article, article, create date and status.
	 * 
	 * @param oldArticle
	 *            the specified old article
	 * @param article
	 *            the specified article
	 * @param createDate
	 *            the specified create date
	 * @param status
	 *            the specified status
	 * @return permalink
	 * @throws Exception
	 *             if duplicated permalink occurs
	 */
	private String getPermalinkForUpdateArticle(final JSONObject oldArticle,
			final JSONObject article, final Date createDate,
			final JSONObject status) throws Exception {
		final String articleId = article.getString(Keys.OBJECT_ID);
		String ret = article.optString(ARTICLE_PERMALINK).trim();
		final String oldPermalink = oldArticle.getString(ARTICLE_PERMALINK);
		if (!oldPermalink.equals(ret)) {
			if (Strings.isEmptyOrNull(ret)) {
				ret = "/articles/" + PERMALINK_FORMAT.format(createDate) + "/"
						+ articleId + ".html";
			}

			if (!ret.startsWith("/")) {
				ret = "/" + ret;
			}

			if (!oldPermalink.equals(ret) && permalinks.exist(ret)) {
				status.put(Keys.CODE,
						StatusCodes.UPDATE_ARTICLE_FAIL_DUPLICATED_PERMALINK);

				throw new Exception(
						"Update article fail, caused by duplicated permalink["
								+ ret + "]");
			}
		}

		return ret;
	}

	/**
	 * Gets the {@link ArticleService} singleton.
	 * 
	 * @return the singleton
	 */
	public static ArticleService getInstance() {
		return SingletonHolder.SINGLETON;
	}

	/**
	 * Private default constructor.
	 */
	private ArticleService() {
	}

	/**
	 * Singleton holder.
	 * 
	 */
	private static final class SingletonHolder {

		/**
		 * Singleton.
		 */
		private static final ArticleService SINGLETON = new ArticleService();

		/**
		 * Private default constructor.
		 */
		private SingletonHolder() {
		}
	}
}
